/* eslint-disable @typescript-eslint/no-explicit-any */
/**
 * Copyright (c) 2012-2015, Christopher Jeffrey, Peter Sunde (MIT License)
 * Copyright (c) 2016, Daniel Imms (MIT License).
 * Copyright (c) 2018, Microsoft Corporation (MIT License).
 */

import { Socket } from 'net';
import { Terminal, DEFAULT_COLS, DEFAULT_ROWS } from '@homebridge/node-pty-prebuilt-multiarch/src/terminal';
import { WindowsPtyAgent } from './windowsPtyAgent';
import { IPtyOpenOptions, IWindowsPtyForkOptions } from '@homebridge/node-pty-prebuilt-multiarch/src/interfaces';
import { ArgvOrCommandLine } from '@homebridge/node-pty-prebuilt-multiarch/src/types';
import { assign } from '@homebridge/node-pty-prebuilt-multiarch/src/utils';

const DEFAULT_FILE = 'cmd.exe';
const DEFAULT_NAME = 'Windows Shell';

export class WindowsTerminal extends Terminal {
    private _isReady: boolean;
    private _deferreds: any[];
    private _agent: WindowsPtyAgent;

    constructor(file?: string, args?: ArgvOrCommandLine, opt?: IWindowsPtyForkOptions) {
        super(opt);

        this._checkType('args', args, 'string', true);

        // Initialize arguments
        args = args || [];
        file = file || DEFAULT_FILE;
        opt = opt || {};
        opt.env = opt.env || process.env;

        if (opt.encoding) {
            console.warn('Setting encoding on Windows is not supported');
        }

        const env = assign({}, opt.env);
        this._cols = opt.cols || DEFAULT_COLS;
        this._rows = opt.rows || DEFAULT_ROWS;
        const cwd = opt.cwd || process.cwd();
        const name = opt.name || env.TERM || DEFAULT_NAME;
        const parsedEnv = this._parseEnv(env);

        // If the terminal is ready
        this._isReady = false;

        // Functions that need to run after `ready` event is emitted.
        this._deferreds = [];

        // Create new termal.
        this._agent = new WindowsPtyAgent(file, args, parsedEnv, cwd, this._cols, this._rows, false, opt.useConpty, opt.useConptyDll, opt.conptyInheritCursor);
        this._socket = this._agent.outSocket;

        // Not available until `ready` event emitted.
        this._pid = this._agent.innerPid;
        this._fd = this._agent.fd;
        this._pty = this._agent.pty;

        // The forked windows terminal is not available until `ready` event is
        // emitted.
        this._socket.on('ready_datapipe', () => {

            // These events needs to be forwarded.
            ['connect', 'data', 'end', 'timeout', 'drain'].forEach(event => {
                this._socket.on(event, () => {

                    // Wait until the first data event is fired then we can run deferreds.
                    if (!this._isReady && event === 'data') {

                        // Terminal is now ready and we can avoid having to defer method
                        // calls.
                        this._isReady = true;

                        // Execute all deferred methods
                        this._deferreds.forEach(fn => {
                            // NB! In order to ensure that `this` has all its references
                            // updated any variable that need to be available in `this` before
                            // the deferred is run has to be declared above this forEach
                            // statement.
                            fn.run();
                        });

                        // Reset
                        this._deferreds = [];

                    }
                });
            });

            // Shutdown if `error` event is emitted.
            this._socket.on('error', err => {
                // Close terminal session.
                this._close();

                // EIO, happens when someone closes our child process: the only process
                // in the terminal.
                // node < 0.6.14: errno 5
                // node >= 0.6.14: read EIO
                if ((<any>err).code) {
                    if (~(<any>err).code.indexOf('errno 5') || ~(<any>err).code.indexOf('EIO')) return;
                }

                // Throw anything else.
                if (this.listeners('error').length < 2) {
                    throw err;
                }
            });

            // Cleanup after the socket is closed.
            this._socket.on('close', () => {
                this.emit('exit', this._agent.exitCode);
                this._close();
            });

        });

        this._file = file;
        this._name = name;

        this._readable = true;
        this._writable = true;

        this._forwardEvents();
    }

    protected _write(data: string): void {
        this._defer(this._doWrite, data);
    }

    private _doWrite(data: string): void {
        this._agent.inSocket.write(data);
    }

    /**
   * openpty
   */

    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    public static open(_options?: IPtyOpenOptions): void {
        throw new Error('open() not supported on windows, use Fork() instead.');
    }

    /**
   * TTY
   */

    public resize(cols: number, rows: number): void {
        if (cols <= 0 || rows <= 0 || isNaN(cols) || isNaN(rows) || cols === Infinity || rows === Infinity) {
            throw new Error('resizing must be done using positive cols and rows');
        }
        this._deferNoArgs(() => {
            this._agent.resize(cols, rows);
            this._cols = cols;
            this._rows = rows;
        });
    }

    public clear(): void {
        this._deferNoArgs(() => {
            this._agent.clear();
        });
    }

    public destroy(): void {
        this._deferNoArgs(() => {
            this.kill();
        });
    }

    public kill(signal?: string): void {
        this._deferNoArgs(() => {
            if (signal) {
                throw new Error('Signals not supported on windows.');
            }
            this._close();
            this._agent.kill();
        });
    }

    // @ts-expect-error - This is a private method that is not part of the public API.
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    private _deferNoArgs<A>(deferredFn: () => void): void {
        // If the terminal is ready, execute.
        if (this._isReady) {
            deferredFn.call(this);
            return;
        }

        // Queue until terminal is ready.
        this._deferreds.push({
            run: () => deferredFn.call(this)
        });
    }

    private _defer<A>(deferredFn: (arg: A) => void, arg: A): void {
        // If the terminal is ready, execute.
        if (this._isReady) {
            deferredFn.call(this, arg);
            return;
        }

        // Queue until terminal is ready.
        this._deferreds.push({
            run: () => deferredFn.call(this, arg)
        });
    }

    public get process(): string { return this._name; }
    public get master(): Socket { throw new Error('master is not supported on Windows'); }
    public get slave(): Socket { throw new Error('slave is not supported on Windows'); }
}
